Android Bluetooth Service

Bluetooth Service
on Android

A complete developer guide to Bluetooth Classic and BLE โ€” from permissions to GATT profiles, scanning, pairing, and data transfer.

What is Android Bluetooth Service?

Android's Bluetooth subsystem provides a framework API to discover, pair, connect, and exchange data with Bluetooth-enabled hardware โ€” classic (BR/EDR) and low-energy (BLE).

๐Ÿ“ก

Bluetooth Classic (BR/EDR)

Basic Rate / Enhanced Data Rate. Used for continuous, high-bandwidth streaming โ€” audio, file transfer, serial emulation (SPP). Operates at 2.4 GHz with up to 3 Mbps throughput.

Audio ยท Serial ยท HID
โšก

Bluetooth Low Energy (BLE)

Optimized for intermittent, small-payload communication. Ideal for sensors, wearables, beacons. Uses GATT profiles. Consumes ~100x less power than classic BT at rest.

Sensors ยท Beacons ยท IoT
๐Ÿ”ง

BluetoothAdapter

The entry point for all Bluetooth operations. A singleton representing the local BT radio. Used to start discovery, query state, get bonded devices, and open RFCOMM sockets.

System Service
๐Ÿ—๏ธ

BluetoothManager

System service class (API 18+). Provides access to BluetoothAdapter and manages connected devices. Obtained via getSystemService(BLUETOOTH_SERVICE).

API 18+

Declaring Bluetooth Permissions

Permission requirements changed significantly with Android 12 (API 31). You must declare the right combination based on your target SDK.

Android โ‰ค 11 (API โ‰ค 30)

<!-- AndroidManifest.xml --> <uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" /> <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" /> <!-- Required for discovery on API 23โ€“30 --> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

Android 12+ (API 31+)

<!-- Scan for other BT devices --> <uses-permission android:name="android.permission.BLUETOOTH_SCAN" /> <!-- Connect to paired/known devices --> <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" /> <!-- Make device discoverable --> <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
BLUETOOTH_SCAN

Grants the ability to discover nearby Bluetooth devices. Required to start BLE scans or trigger classic device discovery. Runtime permission on API 31+.

BLUETOOTH_CONNECT

Allows connecting to bonded devices, reading device names, and managing RFCOMM/GATT connections. Runtime permission โ€” must be requested at runtime.

BLUETOOTH_ADVERTISE

Required to advertise your device as discoverable to others (BLE peripheral role). Not needed for most client-only apps.

ACCESS_FINE_LOCATION (โ‰ค API 30)

BT discovery could infer physical location, so location permission was enforced. On API 31+, use BLUETOOTH_SCAN with neverForLocation if no location inference occurs.


Bluetooth Adapter States

The BluetoothAdapter cycles through well-defined states. Monitor them via BroadcastReceiver to react to hardware changes in real time.

ConstantValueMeaningStatus
STATE_OFF10Bluetooth radio is fully offInactive
STATE_TURNING_ON11Radio powering up (transitional)Transitioning
STATE_ON12Bluetooth is fully on and operationalActive
STATE_TURNING_OFF13Radio powering down (transitional)Transitioning
// Register a BroadcastReceiver to monitor state changes val receiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val state = intent.getIntExtra( BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR ) when (state) { BluetoothAdapter.STATE_ON -> onBluetoothEnabled() BluetoothAdapter.STATE_OFF -> onBluetoothDisabled() } } } registerReceiver(receiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED))

Scanning & Discovery

Android provides two separate scanning mechanisms โ€” classic inquiry and BLE scan. Each is suited to different hardware and use cases.

Step 1

Check & Request Permissions

Before any scan, verify BLUETOOTH_SCAN (API 31+) or ACCESS_FINE_LOCATION (API โ‰ค 30) is granted. Use ActivityResultContracts.RequestMultiplePermissions for runtime requests.

Step 2

Classic Discovery via startDiscovery()

Triggers a 12-second inquiry scan for classic BR/EDR devices. Results arrive via ACTION_FOUND broadcast. Heavyweight โ€” interrupts active connections. Each found device returns a BluetoothDevice object with name, address, and class.

Step 3

BLE Scan via BluetoothLeScanner

Access via adapter.bluetoothLeScanner. Call startScan() with optional ScanFilter (filter by service UUID, device name, MAC) and ScanSettings (SCAN_MODE_LOW_LATENCY, LOW_POWER, or BALANCED). Results arrive in ScanCallback.onScanResult().

Step 4

Stop Scans Promptly

Classic discovery is battery-heavy. Always call cancelDiscovery() before connecting. BLE scans auto-stop after ~30 min in background; explicitly call stopScan() in onPause/onDestroy to conserve battery.

Step 5

Handle Already-Bonded Devices

Use adapter.bondedDevices to get the Set<BluetoothDevice> of previously paired devices. No scan needed to connect to these โ€” check by name or UUID and open a socket directly.


Classic Connection & RFCOMM

Classic Bluetooth connections use RFCOMM โ€” a serial-port emulation protocol โ€” over a BluetoothSocket. All I/O must happen off the main thread.

// Client: connect to a remote device via SPP UUID val sppUUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB") val socket: BluetoothSocket = device .createRfcommSocketToServiceRecord(sppUUID) // Run on a background thread (e.g. Coroutine / Thread) withContext(Dispatchers.IO) { adapter.cancelDiscovery() // ALWAYS cancel discovery first socket.connect() // Blocks until connected or throws val input = socket.inputStream val output = socket.outputStream output.write("Hello".toByteArray()) val buffer = ByteArray(1024) val bytes = input.read(buffer) }
๐Ÿ–ฅ๏ธ

Server Role (Accept)

Use adapter.listenUsingRfcommWithServiceRecord(name, uuid) to create a BluetoothServerSocket. Call accept() (blocking) in a background thread. After a client connects, you get a BluetoothSocket for I/O.

๐Ÿ“ฒ

Client Role (Connect)

Obtain the remote BluetoothDevice, call createRfcommSocketToServiceRecord(uuid). Run connect() on a background thread โ€” it blocks for ~12 seconds before timing out.

๐Ÿ”’

Secure vs Insecure

createRfcommSocketToServiceRecord() requires pairing. Use createInsecureRfcommSocketToServiceRecord() to skip pairing (no MITM protection). Use secure for sensitive data.


BLE: GATT Profiles & Characteristics

BLE communication is structured via the GATT protocol โ€” a hierarchy of Services, Characteristics, and Descriptors identified by UUIDs.

๐Ÿ—‚๏ธ

GATT Profile

A formal specification defining how two BLE devices use services to communicate. Examples: Heart Rate Profile, Battery Service Profile. Each profile contains one or more Services.

โš™๏ธ

Service

A collection of data and associated behaviors. Identified by a 128-bit UUID (or short 16-bit SIG UUID). E.g., Heart Rate Service = 0x180D. A device exposes multiple services.

๐Ÿ“

Characteristic

A data value within a Service. Has properties: READ, WRITE, NOTIFY, INDICATE. E.g., Heart Rate Measurement characteristic = 0x2A37. This is where your actual sensor data lives.

๐Ÿ“Œ

Descriptor

Metadata about a Characteristic. The most important is CCCD (0x2902) โ€” the Client Characteristic Configuration Descriptor โ€” which you write to enable NOTIFY or INDICATE.

// Connect to BLE device and discover services val gatt = device.connectGatt(context, false, object : BluetoothGattCallback() { override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { if (newState == BluetoothProfile.STATE_CONNECTED) { gatt.discoverServices() // Trigger service discovery after connect } } override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { val service = gatt.getService(MY_SERVICE_UUID) val char = service.getCharacteristic(MY_CHAR_UUID) gatt.readCharacteristic(char) } override fun onCharacteristicRead(...) { val data = characteristic.value // Raw ByteArray } override fun onCharacteristicChanged(...) { // Called when NOTIFY fires โ€” real-time updates } })

Bonding Process

Bonding = pairing + key storage. Android handles the UI, but you must react to bonding state changes to know when it's safe to connect.

BOND_NONE (10)

Device has never been paired or bond was removed. Call createBond() to initiate pairing. Android shows system pairing dialog automatically.

BOND_BONDING (11)

Pairing is in progress. Do NOT attempt to connect. Listen for ACTION_BOND_STATE_CHANGED broadcast and wait for BOND_BONDED before opening a socket.

BOND_BONDED (12)

Pairing complete. Link keys are stored. You can now open an RFCOMM or GATT connection. Bonded devices persist across reboots in adapter.bondedDevices.


Critical Tips for Production

Bluetooth on Android has many sharp edges. These practices prevent the most common bugs and crashes.

๐Ÿ”‹

Always Cancel Discovery

Classic discovery significantly degrades connection speed and battery. Call adapter.cancelDiscovery() before any socket.connect() call โ€” even if you didn't start discovery yourself.

๐Ÿงต

Never Block the Main Thread

All BT I/O โ€” connect(), read(), write() โ€” is blocking. Use Kotlin Coroutines with Dispatchers.IO, or a HandlerThread. BluetoothGattCallback also arrives on a Binder thread, not Main.

๐Ÿ”

Serialize GATT Operations

BluetoothGatt is not thread-safe. Never call readCharacteristic(), writeCharacteristic(), or setCharacteristicNotification() concurrently. Use a queue and wait for each callback before the next operation.

๐Ÿ”Œ

Close Resources Properly

Always call socket.close() and gatt.close() (after gatt.disconnect()) in a finally block. Leaked sockets and GATT connections cause "Too many open files" crashes and device-side resource exhaustion.

๐Ÿ“ก

Use MTU Negotiation for BLE

Default BLE MTU is 23 bytes (20 bytes payload). Call gatt.requestMtu(512) after connection to increase throughput. Handle the result in onMtuChanged() before large writes.

๐Ÿ›ก๏ธ

Handle API 31+ Carefully

Every Bluetooth API call โ€” including adapter.name, bondedDevices โ€” throws SecurityException on API 31+ if BLUETOOTH_CONNECT is not granted. Wrap all calls or check permissions defensively.